Skip to content

Conversation

@ArnabChatterjee20k
Copy link
Contributor

@ArnabChatterjee20k ArnabChatterjee20k commented Jan 19, 2026

Multi-type db was reverted
So re-reverting the commits

Fix Double-Counting of row/document Migration Status Counters

Problem

E2E test testAppwriteMigrationMixedDatabases was failing with incorrect statusCounters (e.g., row.success = 2 when only 1 row was migrated, or pending going negative). Debug logs from Cache::add() showed the same row/document IDs being processed twice within a single migration run.

Root Cause: Source + Destination Both Updated Cache for Rows/Documents

Rows and documents are special-cased in Cache to store aggregated counters per status (as strings), not per-resource objects. But they were being counted twice:

  1. Destination always calls cache->update($responseResource) during import
  2. Source also calls cache->addAll($prunedResources) after destination callback returns

For row and document, Cache::update() delegates to Cache::add(), which increments the aggregated counter. So if source also adds them, it becomes double counting.

Evidence in Code (Before Fix)

Source adds exported resources to cache after destination callback:

// vendor/utopia-php/migration/src/Migration/Source.php:60-81
$callback($returnedResources);
$this->cache->addAll($prunedResources);

Destination updates cache for every imported resource:

// vendor/utopia-php/migration/src/Migration/Destinations/Appwrite.php:240-281
foreach ($resources as $index => $resource) {
    // ...
    $responseResource = match ($resource->getGroup()) {
        Transfer::GROUP_DATABASES => $this->importDatabaseResource($resource, $isLast),
        // ...
    };
    // ...
    $this->cache->update($responseResource);
}

Rows/documents are aggregated in cache, and update() calls add() for them:

// vendor/utopia-php/migration/src/Migration/Cache.php:88-99
public function add(Resource $resource): void
{
    if ($resource->getName() == Resource::TYPE_ROW || $resource->getName() == Resource::TYPE_DOCUMENT) {
        $status = $resource->getStatus();
        $counter = $this->cache[$resource->getName()][$status] ?? 0;
        $counter = intval($counter) + 1;
        $this->cache[$resource->getName()][$status] = $counter . '';
        return;
    }
    // ...
}
// vendor/utopia-php/migration/src/Migration/Cache.php:143-146
public function update(Resource $resource): void
{
    if ($resource->getName() == Resource::TYPE_ROW || $resource->getName() == Resource::TYPE_DOCUMENT) {
        $this->add($resource);  // ← increments counter again
        return;
    }
    // ...
}

Solution

Skip source-side cache population for row/document - destination import is already the single source of truth for these counters:

// vendor/utopia-php/migration/src/Migration/Source.php:67-74
/**
 * Rows and documents are counted in cache in an aggregated way (per status count)
 * to prevent memory blowups. Destination updates already account for these, and
 * adding them here causes double counting (source + destination).
 */
if (\in_array($resource->getName(), [Resource::TYPE_ROW, Resource::TYPE_DOCUMENT], true)) {
    continue;
}
$prunedResources[] = $resource;

Also clamp row/document pending subtraction to prevent negative values if counts drift:

// vendor/utopia-php/migration/src/Migration/Transfer.php:174-186
if ($status[$resourceType]['pending'] > 0) {
    $status[$resourceType]['pending'] -= \min($status[$resourceType]['pending'], $resource);
}

Summary by CodeRabbit

  • New Features

    • Multi-database migration support added for DocumentsDB and VectorDB.
    • Many new attribute/column types (vector, object, enum, relationship, boolean, datetime, decimal, integer, email, ip, url, point/line/polygon, etc.).
  • Changes

    • Export/import and reader flows run per-database with richer entity/record/field payloads and dynamic type routing.
    • CSV export improved for complex value shapes and document/collection exports.
    • Cache keys now include database type; source counting adjusted to avoid double-counting.

This reverts commit 7fcb7a1.
This reverts commit 2dc0108.
This reverts commit be4d49d.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 19, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds multi-database support across the migration subsystem: introduces new database resource types (DocumentsDB, VectorDB, legacy), Collection and Document models, and DATABASE/ENTITY/FIELD/RECORD mapping constants. Adds an abstract Attribute class and many concrete attribute/column types (text, integer, decimal, enum, vector, object, geo types, relationship, boolean, datetime, email, ip, url). Refactors Appwrite source/reader and Appwrite destination to use per-database readers/callables and per-database DSNs, updates CSV export to emit collection/document/attribute payloads, and appends database type into resource cache keys.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Multitype db' is vague and generic, using non-descriptive abbreviated terminology that does not clearly convey the specific purpose of the changeset to someone reviewing the history. Consider using a more descriptive title such as 'Support multiple database types (TableDB, DocumentDB, VectorDB)' or 'Add multi-database type support with double-counting fix' to better reflect the primary changes.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch multitype-db

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/Migration/Destinations/Appwrite.php (1)

352-363: Normalize database type before DSN resolution.

The type field normalizes empty getType() to 'legacy', but the DSN lookup receives getType() directly, which could be an empty string. If the DSN resolver doesn't handle empty strings, database metadata can point to the wrong DSN.

Suggested fix
+        $databaseType = $resource->getType() ?: 'legacy';
         $database = $this->dbForProject->createDocument('databases', new UtopiaDocument([
             '$id' => $resource->getId(),
             'name' => $resource->getDatabaseName(),
             'enabled' => $resource->getEnabled(),
             'search' => implode(' ', [$resource->getId(), $resource->getDatabaseName()]),
             '$createdAt' => $resource->getCreatedAt(),
             '$updatedAt' => $resource->getUpdatedAt(),
             'originalId' => empty($resource->getOriginalId()) ? null : $resource->getOriginalId(),
-            'type' => empty($resource->getType()) ? 'legacy' : $resource->getType(),
+            'type' => $databaseType,
             // source and destination can be in different location
-            'database' => ($this->getDatabaseDSN)($resource->getType())
+            'database' => ($this->getDatabaseDSN)($databaseType)
         ]));
🤖 Fix all issues with AI agents
In `@src/Migration/Destinations/Appwrite.php`:
- Around line 459-464: The guard in createField(Column|Attribute $resource)
currently checks Resource::TYPE_DATABASE_DOCUMENTSDB and sets status with
message "Columns not supported for DocumentsDB" but the method receives both
Column and Attribute; either narrow the condition to only apply when $resource
is an instance of Column, or change the status message to accurately reflect
both types (e.g., "Columns and Attributes not supported for DocumentsDB" or
"Fields not supported for DocumentsDB"); update the check around createField and
the call to $resource->setStatus(Resource::STATUS_SKIPPED, ...) accordingly so
the behavior and message match (refer to createField, Column, Attribute,
Resource::TYPE_DATABASE_DOCUMENTSDB, setStatus, STATUS_SKIPPED).

In `@src/Migration/Resources/Database/Attribute/Decimal.php`:
- Around line 22-23: The current defaulting in Decimal.php uses PHP_FLOAT_MIN
for $min which is the smallest positive float, not a negative bound; update the
default to use -PHP_FLOAT_MAX (or set $min ??= -PHP_FLOAT_MAX) so decimal ranges
allow negative values, keeping $max ??= PHP_FLOAT_MAX unchanged and adjust any
references to $min/$max in the Decimal class or related validation to expect a
potentially negative lower bound.

In `@src/Migration/Resources/Database/Attribute/Integer.php`:
- Around line 78-91: The fromArray method for class Integer is not
reading/passing the constructor's $signed parameter, causing unsigned integers
to be lost; update Integer::fromArray to extract the signed flag (e.g.
$array['signed'] ?? false) and pass it into the Integer constructor (the
__construct parameter named $signed), and also update the method PHPDoc to
document the signed field being deserialized.

In `@src/Migration/Resources/Database/Attribute/Polygon.php`:
- Around line 58-67: In Polygon::fromArray change the handling of the 'default'
index to guard against missing keys: update the constructor call in the
fromArray method to pass $array['default'] ?? null (matching other attribute
factories' null fallback) so an undefined index notice is avoided; leave other
fields (required, createdAt, updatedAt) as-is and locate this in the fromArray
method of the Polygon class to apply the fix.

In `@src/Migration/Resources/Database/VectorDB.php`:
- Around line 14-24: The PHPDoc for the array parameter is missing the "type"
key even though the code reads $array['type'] (see use in the VectorDB
class/method handling that array); update the `@param` array shape to include
type: string so the documented shape matches runtime access (add "type: string"
to the array shape in the PHPDoc for the $array parameter).

In `@src/Migration/Sources/Appwrite.php`:
- Around line 851-857: The array construction is using $databaseName (which
holds the database type) for the 'name' field; update the assignment to use the
actual database name accessor on the $database object (e.g., replace
$databaseName with $database->getDatabaseName() or $database->getName()
depending on the available method) inside the block that builds the 'database'
array so the 'name' value reflects the real database name rather than its type.

In `@src/Migration/Sources/Appwrite/Reader/Database.php`:
- Around line 27-38: The Database class is missing implementations for
getDatabase() and logDebug() which are invoked from within the class; add a
private getDatabase(): UtopiaDatabase method that returns the correct DB
instance by invoking the stored callable $this->getDatabasesDB with
$this->projectId when callable (and falling back to $this->dbForProject), and
add a private logDebug(string $message, array $context = []): void method that
emits debug output (format the message with context) using an existing logger if
present (e.g., $this->logger) or error_log as a fallback so existing calls to
getDatabase() and logDebug() succeed.

In `@src/Migration/Transfer.php`:
- Line 361: The variable $groupDatabasesIndex is assigned from
array_search(Transfer::GROUP_DATABASES, $services) but never used; remove the
unused assignment line to eliminate the dead variable (i.e. delete the statement
that sets $groupDatabasesIndex) so only relevant variables and logic remain; if
the intent was to check presence of Transfer::GROUP_DATABASES in $services
instead, replace the assignment with an explicit conditional using
in_array(Transfer::GROUP_DATABASES, $services) where needed.
🧹 Nitpick comments (13)
src/Migration/Resources/Database/Database.php (1)

62-72: Consider including database in jsonSerialize() for round-trip consistency.

The new $database property is not included in jsonSerialize(). If resources are serialized and later reconstructed via fromArray(), the database value will be lost. If this is intentional (e.g., database is only for internal routing), consider documenting it; otherwise, add it to the serialized output.

Suggested fix (if round-trip is needed)
     public function jsonSerialize(): array
     {
         return [
             'id' => $this->id,
             'name' => $this->name,
             'createdAt' => $this->createdAt,
             'updatedAt' => $this->updatedAt,
             'enabled' => $this->enabled,
             'type' => $this->type,
+            'database' => $this->database,
         ];
     }
src/Migration/Resources/Database/Attribute/Vector.php (1)

61-72: Minor formatting nit: missing space after colon.

Line 67 has size:$array['size'] without a space after the colon, unlike other named arguments. This is inconsistent with the rest of the codebase formatting.

🔧 Suggested fix
     public static function fromArray(array $array): self
     {
         return new self(
             $array['key'],
             Collection::fromArray($array['table'] ?? $array['collection']),
             required: $array['required'],
-            size:$array['size'],
+            size: $array['size'],
             default: $array['default'],
             createdAt: $array['createdAt'] ?? '',
             updatedAt: $array['updatedAt'] ?? '',
         );
     }
src/Migration/Resources/Database/Columns/Vector.php (1)

61-72: Minor formatting nit: missing space after colon.

Line 67 has size:$array['size'] without a space after the colon, inconsistent with other named arguments in the file and codebase.

🔧 Suggested fix
     public static function fromArray(array $array): self
     {
         return new self(
             $array['key'],
             Table::fromArray($array['table'] ?? $array['collection']),
             required: $array['required'],
-            size:$array['size'],
+            size: $array['size'],
             default: $array['default'],
             createdAt: $array['createdAt'] ?? '',
             updatedAt: $array['updatedAt'] ?? '',
         );
     }
src/Migration/Resources/Database/Attribute/Text.php (1)

88-96: Redundant method overrides.

Per the relevant code snippet from src/Migration/Resources/Database/Attribute.php, the base Attribute class already defines getSize() and getFormat() with identical implementations. These overrides can be removed.

♻️ Suggested removal
     public function getType(): string
     {
         return Attribute::TYPE_STRING;
     }
-
-    public function getSize(): int
-    {
-        return $this->size;
-    }
-
-    public function getFormat(): string
-    {
-        return $this->format;
-    }
 }
src/Migration/Resources/Database/DocumentsDB.php (1)

14-24: PHPDoc is missing the type key.

The implementation uses $array['type'] ?? 'legacy' on line 34, but the PHPDoc shape definition doesn't include the type key. Consider adding it for accuracy.

📝 Suggested fix
     /**
      * `@param` array{
      *     id: string,
      *     name: string,
      *     createdAt: string,
      *     updatedAt: string,
      *     enabled: bool,
      *     originalId: string|null,
+     *     type: string,
      *     database: string
      * } $array
      */
src/Migration/Resources/Database/Attribute/Enum.php (1)

98-101: Unnecessary array cast.

The formatOptions['elements'] is always an array by construction (set in constructor line 33 and guaranteed by PHPDoc). The (array) cast is redundant.

♻️ Suggested simplification
     public function getElements(): array
     {
-        return (array)$this->formatOptions['elements'];
+        return $this->formatOptions['elements'];
     }
src/Migration/Resources/Database/Attribute/Integer.php (1)

98-106: Return type ?int is misleading.

Both getMin() and getMax() are declared as returning ?int, but they always return an int due to the cast and because the constructor defaults min/max to PHP_INT_MIN/PHP_INT_MAX. Consider changing the return type to int for accuracy, or removing the cast if null should actually be possible.

♻️ Suggested fix (if null is never expected)
-    public function getMin(): ?int
+    public function getMin(): int
     {
         return (int)$this->formatOptions['min'];
     }

-    public function getMax(): ?int
+    public function getMax(): int
     {
         return (int)$this->formatOptions['max'];
     }
src/Migration/Resources/Database/Attribute/Decimal.php (1)

96-104: Return type inconsistency in getMin/getMax.

The return type is ?float (nullable), but the implementation always casts to (float), which will never return null. Either remove the nullable return type or handle the null case.

Proposed fix
     public function getMin(): ?float
     {
-        return (float)$this->formatOptions['min'];
+        return isset($this->formatOptions['min']) ? (float)$this->formatOptions['min'] : null;
     }

     public function getMax(): ?float
     {
-        return (float)$this->formatOptions['max'];
+        return isset($this->formatOptions['max']) ? (float)$this->formatOptions['max'] : null;
     }
src/Migration/Resources/Database/Attribute/Relationship.php (1)

96-99: Minor: getRelatedTable() fallback is effectively dead code.

Since the constructor always stores the value under 'relatedCollection', the 'relatedTable' key will never exist in $this->options. The null coalescing fallback works correctly, but the first check is unnecessary.

Optional simplification
     public function getRelatedTable(): string
     {
-        return $this->options['relatedTable'] ?? $this->options['relatedCollection'];
+        return $this->options['relatedCollection'];
     }

However, keeping the fallback may be intentional for compatibility if fromArray is used with data containing 'relatedTable'.

src/Migration/Sources/Appwrite.php (1)

92-95: PHPDoc typo: UtopiaDocument should be a valid type.

The type hint UtopiaDocument appears to be incomplete. It should likely be \Utopia\Database\Document or similar.

Proposed fix
     /**
-     * `@var` callable(UtopiaDocument $database|null): UtopiaDatabase
+     * `@var` callable(\Utopia\Database\Document|null $database): UtopiaDatabase
      */
     protected mixed $getDatabasesDB;
src/Migration/Destinations/Appwrite.php (3)

797-808: Drop unused $supportForSpatialAttributes (or pass into validator).
PHPMD flagged it as unused; keeping it adds noise.

♻️ Suggested cleanup
-        $supportForSpatialAttributes = $dbForDatabases->getAdapter()->getSupportForSpatialAttributes();

962-975: Avoid unused $value in the row scrub loop.
Small cleanup to satisfy PHPMD.

♻️ Suggested cleanup
-                        foreach ($row as $key => $value) {
+                        foreach ($row as $key => $_) {

1522-1525: Remove unused $columnSize.
PHPMD flags this; drop it unless you plan to use size for index lengths.

♻️ Suggested cleanup
-            $columnSize = $oldColumns[$columnIndex]['size'];

Comment on lines +22 to +23
$min ??= PHP_FLOAT_MIN;
$max ??= PHP_FLOAT_MAX;
Copy link
Contributor

@coderabbitai coderabbitai bot Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Potential issue with PHP_FLOAT_MIN as minimum bound.

PHP_FLOAT_MIN represents the smallest positive float (~2.2e-308), not a negative minimum. This differs from Integer.php which correctly uses PHP_INT_MIN (a negative value). For decimal/float ranges, if you intend to allow negative values, consider using -PHP_FLOAT_MAX as the lower bound.

Proposed fix
-        $min ??= PHP_FLOAT_MIN;
+        $min ??= -PHP_FLOAT_MAX;
🤖 Prompt for AI Agents
In `@src/Migration/Resources/Database/Attribute/Decimal.php` around lines 22 - 23,
The current defaulting in Decimal.php uses PHP_FLOAT_MIN for $min which is the
smallest positive float, not a negative bound; update the default to use
-PHP_FLOAT_MAX (or set $min ??= -PHP_FLOAT_MAX) so decimal ranges allow negative
values, keeping $max ??= PHP_FLOAT_MAX unchanged and adjust any references to
$min/$max in the Decimal class or related validation to expect a potentially
negative lower bound.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No it is same as column/integer

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the Columns/Decimal.php implementation for min/max defaults
cat src/Migration/Resources/Database/Columns/Decimal.php | grep -A 30 "function __construct"

Length of output: 775


@ArnabChatterjee20k, you're absolutely right! I've verified that Columns/Decimal.php uses the identical pattern with PHP_FLOAT_MIN for the min default. The implementations are intentionally consistent, which aligns with the established pattern in this codebase where Attribute classes mirror their corresponding Column classes in constructor defaults.

I apologize for the confusion in my original review comment—it didn't account for this intentional consistency pattern. Thank you for the clarification!


🧠 Learnings used
Learnt from: ArnabChatterjee20k
Repo: utopia-php/migration PR: 114
File: src/Migration/Resources/Database/Attribute/Decimal.php:10-38
Timestamp: 2025-11-20T13:07:39.293Z
Learning: In the utopia-php/migration codebase, the user ArnabChatterjee20k maintains consistency between parallel implementations: Attribute classes (e.g., Attribute/Decimal.php) mirror their corresponding Column classes (e.g., Columns/Decimal.php) in constructor defaults and behavior, even when design decisions might be questioned.

Learnt from: ItzNotABug
Repo: utopia-php/migration PR: 81
File: src/Migration/Sources/CSV.php:215-233
Timestamp: 2025-07-02T06:24:50.844Z
Learning: In the utopia-php/migration codebase, invalid numeric values (like null) in parsed data are acceptable because the underlying database structure validator will catch and handle these validation errors, so explicit filtering during parsing is not required.

Learnt from: ItzNotABug
Repo: utopia-php/migration PR: 80
File: src/Migration/Sources/Appwrite.php:843-851
Timestamp: 2025-06-28T09:47:08.333Z
Learning: In the utopia-php/migration codebase, during the terminology swap from Collection/Attribute/Document to Table/Column/Row, the class constructors and method parameters use the new terminology (like "relatedTable"), but the underlying data structures and API responses still use the legacy keys (like "relatedCollection"). This is an intentional design pattern to allow gradual migration while maintaining compatibility with existing data sources.

Comment on lines 78 to 91
public static function fromArray(array $array): self
{
return new self(
$array['key'],
Collection::fromArray($array['table'] ?? $array['collection']),
required: $array['required'],
default: $array['default'],
array: $array['array'],
min: $array['formatOptions']['min'] ?? null,
max: $array['formatOptions']['max'] ?? null,
createdAt: $array['createdAt'] ?? '',
updatedAt: $array['updatedAt'] ?? '',
);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing signed parameter in fromArray.

The constructor accepts a $signed parameter (line 18), but fromArray doesn't extract or pass it. This causes data loss during deserialization—unsigned integers will be incorrectly reconstructed as signed.

🐛 Suggested fix
     public static function fromArray(array $array): self
     {
         return new self(
             $array['key'],
             Collection::fromArray($array['table'] ?? $array['collection']),
             required: $array['required'],
             default: $array['default'],
             array: $array['array'],
             min: $array['formatOptions']['min'] ?? null,
             max: $array['formatOptions']['max'] ?? null,
+            signed: $array['signed'] ?? true,
             createdAt: $array['createdAt'] ?? '',
             updatedAt: $array['updatedAt'] ?? '',
         );
     }

Also update the PHPDoc to include:

      *     formatOptions: array{
      *         min: ?int,
      *         max: ?int
      *     },
+     *     signed: bool,
      *     createdAt: string,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public static function fromArray(array $array): self
{
return new self(
$array['key'],
Collection::fromArray($array['table'] ?? $array['collection']),
required: $array['required'],
default: $array['default'],
array: $array['array'],
min: $array['formatOptions']['min'] ?? null,
max: $array['formatOptions']['max'] ?? null,
createdAt: $array['createdAt'] ?? '',
updatedAt: $array['updatedAt'] ?? '',
);
}
public static function fromArray(array $array): self
{
return new self(
$array['key'],
Collection::fromArray($array['table'] ?? $array['collection']),
required: $array['required'],
default: $array['default'],
array: $array['array'],
min: $array['formatOptions']['min'] ?? null,
max: $array['formatOptions']['max'] ?? null,
signed: $array['signed'] ?? true,
createdAt: $array['createdAt'] ?? '',
updatedAt: $array['updatedAt'] ?? '',
);
}
🤖 Prompt for AI Agents
In `@src/Migration/Resources/Database/Attribute/Integer.php` around lines 78 - 91,
The fromArray method for class Integer is not reading/passing the constructor's
$signed parameter, causing unsigned integers to be lost; update
Integer::fromArray to extract the signed flag (e.g. $array['signed'] ?? false)
and pass it into the Integer constructor (the __construct parameter named
$signed), and also update the method PHPDoc to document the signed field being
deserialized.

Comment on lines 58 to 67
public static function fromArray(array $array): self
{
return new self(
$array['key'],
Collection::fromArray($array['table'] ?? $array['collection']),
required: $array['required'],
default: $array['default'],
createdAt: $array['createdAt'] ?? '',
updatedAt: $array['updatedAt'] ?? '',
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Guard against missing default key.
If default is absent, this raises an undefined index notice. Use a null fallback for consistency with other attribute factories.

🛠️ Proposed fix
-            default: $array['default'],
+            default: $array['default'] ?? null,
🤖 Prompt for AI Agents
In `@src/Migration/Resources/Database/Attribute/Polygon.php` around lines 58 - 67,
In Polygon::fromArray change the handling of the 'default' index to guard
against missing keys: update the constructor call in the fromArray method to
pass $array['default'] ?? null (matching other attribute factories' null
fallback) so an undefined index notice is avoided; leave other fields (required,
createdAt, updatedAt) as-is and locate this in the fromArray method of the
Polygon class to apply the fix.

Comment on lines 14 to 24
/**
* @param array{
* id: string,
* name: string,
* createdAt: string,
* updatedAt: string,
* enabled: bool,
* originalId: string|null,
* database: string
* } $array
*/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

PHPDoc is missing the type key.

The @param array shape documents database: string but omits the type key, even though line 34 accesses $array['type']. Add type: string to the PHPDoc for consistency.

Suggested fix
     /**
      * `@param` array{
      *     id: string,
      *     name: string,
      *     createdAt: string,
      *     updatedAt: string,
      *     enabled: bool,
      *     originalId: string|null,
+     *     type: string,
      *     database: string
      * } $array
      */
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* @param array{
* id: string,
* name: string,
* createdAt: string,
* updatedAt: string,
* enabled: bool,
* originalId: string|null,
* database: string
* } $array
*/
/**
* `@param` array{
* id: string,
* name: string,
* createdAt: string,
* updatedAt: string,
* enabled: bool,
* originalId: string|null,
* type: string,
* database: string
* } $array
*/
🤖 Prompt for AI Agents
In `@src/Migration/Resources/Database/VectorDB.php` around lines 14 - 24, The
PHPDoc for the array parameter is missing the "type" key even though the code
reads $array['type'] (see use in the VectorDB class/method handling that array);
update the `@param` array shape to include type: string so the documented shape
matches runtime access (add "type: string" to the array shape in the PHPDoc for
the $array parameter).

Comment on lines 851 to 857
'database' => [
'id' => $database->getId(),
'name' => $databaseName,
'type' => $database->getType(),
'database' => $database->getDatabase(),
]
]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Potential bug: 'name' => $databaseName passes the database type instead of actual name.

At line 853, $databaseName is the database type string (e.g., 'database', 'documentsdb'), not the actual database name. This should likely be $database->getDatabaseName() or similar.

Proposed fix
                     $newTable = self::getEntity($databaseName, [
                         'id' => $table['$id'],
                         'name' => $table['name'],
                         'documentSecurity' => $table['documentSecurity'],
                         'permissions' => $table['$permissions'],
                         'createdAt' => $table['$createdAt'],
                         'updatedAt' => $table['$updatedAt'],
                         'database' => [
                             'id' => $database->getId(),
-                            'name' => $databaseName,
+                            'name' => $database->getDatabaseName(),
                             'type' => $database->getType(),
                             'database' => $database->getDatabase(),
                         ]
                     ]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
'database' => [
'id' => $database->getId(),
'name' => $databaseName,
'type' => $database->getType(),
'database' => $database->getDatabase(),
]
]);
'database' => [
'id' => $database->getId(),
'name' => $database->getDatabaseName(),
'type' => $database->getType(),
'database' => $database->getDatabase(),
]
]);
🤖 Prompt for AI Agents
In `@src/Migration/Sources/Appwrite.php` around lines 851 - 857, The array
construction is using $databaseName (which holds the database type) for the
'name' field; update the assignment to use the actual database name accessor on
the $database object (e.g., replace $databaseName with
$database->getDatabaseName() or $database->getName() depending on the available
method) inside the block that builds the 'database' array so the 'name' value
reflects the real database name rather than its type.

Comment on lines +27 to 38
/**
* @var callable(UtopiaDocument|null): UtopiaDatabase
*/
private mixed $getDatabasesDB;

public function __construct(
private readonly UtopiaDatabase $dbForProject,
?callable $getDatabasesDB = null,
private readonly ?string $projectId = null
) {
$this->getDatabasesDB = $getDatabasesDB;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify getDatabase and logDebug methods are defined in Reader interface or related files
echo "=== Checking Reader interface ==="
cat src/Migration/Sources/Appwrite/Reader.php 2>/dev/null || echo "Reader.php not found"

echo ""
echo "=== Searching for getDatabase method definition ==="
rg -n 'function getDatabase\(' --type=php -C2

echo ""
echo "=== Searching for logDebug method definition ==="
rg -n 'function logDebug\(' --type=php -C2

Repository: utopia-php/migration

Length of output: 4579


🏁 Script executed:

#!/bin/bash
# Check the Database.php class definition (parent class, traits, etc.)
echo "=== Checking Database.php class definition ==="
head -60 src/Migration/Sources/Appwrite/Reader/Database.php

echo ""
echo "=== Searching for logDebug in the entire codebase with context ==="
rg -i 'logdebug' --type=php -C2

echo ""
echo "=== Checking if Database class extends a parent or uses traits ==="
rg -n 'class Database' src/Migration/Sources/Appwrite/Reader/Database.php -A 5

echo ""
echo "=== Checking if getDatabase is called in Database.php and what parameters it takes ==="
rg -n 'getDatabase' src/Migration/Sources/Appwrite/Reader/Database.php -B2 -A2

Repository: utopia-php/migration

Length of output: 9920


Fix missing method implementations: getDatabase() and logDebug()

The Database class calls $this->getDatabase() at lines 343, 398, and 496, and $this->logDebug() at lines 346 and 348, but these methods do not exist. The class implements only the Reader interface, has no parent class, and no traits that would provide these methods. The logDebug() method does not exist anywhere in the codebase, and getDatabase() is not defined in this class, its interface, or any parent. Either implement these methods in the Database class or provide them through a parent class or trait.

🤖 Prompt for AI Agents
In `@src/Migration/Sources/Appwrite/Reader/Database.php` around lines 27 - 38, The
Database class is missing implementations for getDatabase() and logDebug() which
are invoked from within the class; add a private getDatabase(): UtopiaDatabase
method that returns the correct DB instance by invoking the stored callable
$this->getDatabasesDB with $this->projectId when callable (and falling back to
$this->dbForProject), and add a private logDebug(string $message, array $context
= []): void method that emits debug output (format the message with context)
using an existing logger if present (e.g., $this->logger) or error_log as a
fallback so existing calls to getDatabase() and logDebug() succeed.

public static function extractServices(array $services): array
{
$resources = [];
$groupDatabasesIndex = array_search(Transfer::GROUP_DATABASES, $services);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove unused variable $groupDatabasesIndex.

This variable is assigned but never used, as flagged by static analysis.

Proposed fix
-        $groupDatabasesIndex = array_search(Transfer::GROUP_DATABASES, $services);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$groupDatabasesIndex = array_search(Transfer::GROUP_DATABASES, $services);
🧰 Tools
🪛 PHPMD (2.15.0)

361-361: Avoid unused local variables such as '$groupDatabasesIndex'. (undefined)

(UnusedLocalVariable)

🤖 Prompt for AI Agents
In `@src/Migration/Transfer.php` at line 361, The variable $groupDatabasesIndex is
assigned from array_search(Transfer::GROUP_DATABASES, $services) but never used;
remove the unused assignment line to eliminate the dead variable (i.e. delete
the statement that sets $groupDatabasesIndex) so only relevant variables and
logic remain; if the intent was to check presence of Transfer::GROUP_DATABASES
in $services instead, replace the assignment with an explicit conditional using
in_array(Transfer::GROUP_DATABASES, $services) where needed.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/Migration/Sources/Appwrite.php (1)

733-745: Root database filter skips VectorDB.

Line 733 only checks TYPE_DATABASE and TYPE_DATABASE_DOCUMENTSDB, so a VectorDB root export won’t be filtered by ID and can export all databases. Add TYPE_DATABASE_VECTORDB to the condition.

🔧 Suggested fix
-            if ($this->rootResourceId !== '' && ($this->rootResourceType === Resource::TYPE_DATABASE || $this->rootResourceType === Resource::TYPE_DATABASE_DOCUMENTSDB)) {
+            if (
+                $this->rootResourceId !== '' &&
+                \in_array($this->rootResourceType, [
+                    Resource::TYPE_DATABASE,
+                    Resource::TYPE_DATABASE_DOCUMENTSDB,
+                    Resource::TYPE_DATABASE_VECTORDB,
+                ], true)
+            ) {
🤖 Fix all issues with AI agents
In `@src/Migration/Sources/Appwrite.php`:
- Around line 1072-1084: The code calls getRecord with the database name instead
of its type which makes record creation default to Row; change the first
argument of getRecord to use $table->getDatabase()->getType() (instead of
getDatabaseName()) so getRecord receives the expected database type; update the
call site in the Appwrite migration code where getRecord(...) is invoked and
ensure related metadata (the rest of the associative array built from $table and
$table->getDatabase()) remains unchanged.
- Around line 991-1034: Remove the unused local variables that PHPMD flags:
delete the $memUsage assignment and the two $timestamp assignments around the
listRows call in the method that iterates table rows (the block using
$this->reader->listRows($table, $queries)); alternatively, if you want to keep
those diagnostics, re-enable the commented $this->logDebugTrackedProject(...)
calls or guard them behind a debug flag so the variables are actually used
(refer to the logDebugTrackedProject call sites and the $this->reader->listRows
invocation when making the change).

In `@src/Migration/Sources/Appwrite/Reader/Database.php`:
- Around line 91-97: The code reads $database->getAttribute('type') and
immediately indexes Resource::DATABASE_TYPE_RESOURCE_MAP[$databaseType], which
can throw an undefined index for missing/unexpected types; update the logic in
the block around $databaseType and $databaseSpecificResources to default to
Resource::TYPE_DATABASE when getAttribute('type') returns null/empty or when
$databaseType is not a key in Resource::DATABASE_TYPE_RESOURCE_MAP (also keep
the existing normalization for Resource::TYPE_DATABASE_LEGACY and
Resource::TYPE_DATABASE_TABLESDB), e.g. compute $databaseType with a fallback
and guard the lookup with isset/array_key_exists before assigning
$databaseSpecificResources so it never indexes an unknown key.
♻️ Duplicate comments (1)
src/Migration/Sources/Appwrite.php (1)

830-842: Database name is set to type string, not actual name.

Line 839 still assigns 'name' => $databaseName, but $databaseName holds the database type (e.g., database, documentsdb) rather than the actual database name. This was already raised previously.

🔧 Suggested fix
-                            'name' => $databaseName,
+                            'name' => $database->getDatabaseName(),

Comment on lines 991 to 1034
$iterationCount++;

$memUsage = round(memory_get_usage(true) / 1024 / 1024, 2);
// $this->logDebugTrackedProject("Table: {$table->getName()} | Iteration: $iterationCount | Memory: {$memUsage}MB | LastRow: " . ($lastRow ? $lastRow->getId() : 'null'));

$queries = [
$this->database->queryLimit($batchSize),
$this->reader->queryLimit($batchSize),
...$this->queries,
];

$rows = [];

if ($lastRow) {
$queries[] = $this->database->queryCursorAfter($lastRow);
$queries[] = $this->reader->queryCursorAfter($lastRow);
}

$selects = ['*', '$id', '$permissions', '$updatedAt', '$createdAt']; // We want relations flat!
$manyToMany = [];

$attributes = $this->cache->get(Column::getName());
foreach ($attributes as $attribute) {
/** @var Relationship $attribute */
if (
$attribute->getTable()->getId() === $table->getId() &&
$attribute->getType() === Column::TYPE_RELATIONSHIP &&
$attribute->getSide() === 'parent' &&
$attribute->getRelationType() == 'manyToMany'
) {
/**
* Blockers:
* we should use but Does not work properly:
* $selects[] = $attribute->getKey() . '.$id';
* when selecting for a relation we get all relations not just the one we were asking.
* when selecting for a relation like select(*, relation.$id) , all relations get resolve
*/
$manyToMany[] = $attribute->getKey();
if ($this->reader->getSupportForAttributes()) {
$attributes = $this->cache->get($fieldName);

foreach ($attributes as $attribute) {
/** @var Relationship $attribute */
if (
$attribute->getTable()->getId() === $table->getId() &&
$attribute->getType() === Column::TYPE_RELATIONSHIP &&
$attribute->getSide() === 'parent' &&
$attribute->getRelationType() == 'manyToMany'
) {
$manyToMany[] = $attribute->getKey();
}
}
}
/** @var Column|Relationship $attribute */

$queries[] = $this->database->querySelect($selects);
$queries[] = $this->reader->querySelect($selects);

$timestamp = microtime(true);
// $this->logDebugTrackedProject("BEFORE listRows() | Table: {$table->getName()} | Batch: $batchSize | Timestamp: {$timestamp}");

$response = $this->database->listRows($table, $queries);
$response = $this->reader->listRows($table, $queries);

$timestamp = microtime(true);
// $this->logDebugTrackedProject("AFTER listRows() | Table: {$table->getName()} | Rows: " . count($response) . " | Timestamp: $timestamp");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove unused local variables to satisfy PHPMD.

$memUsage (Line 993) and $timestamp (Lines 1028/1033) are computed but never used because the debug logs are commented out. PHPMD flags these as unused locals. Either re-enable logging behind a flag or remove the variables.

🔧 Suggested cleanup
-                $memUsage = round(memory_get_usage(true) / 1024 / 1024, 2);
-                // $this->logDebugTrackedProject("Table: {$table->getName()} | Iteration: $iterationCount | Memory: {$memUsage}MB | LastRow: " . ($lastRow ? $lastRow->getId() : 'null'));
+                // $this->logDebugTrackedProject("Table: {$table->getName()} | Iteration: $iterationCount | Memory: " . round(memory_get_usage(true) / 1024 / 1024, 2) . "MB | LastRow: " . ($lastRow ? $lastRow->getId() : 'null'));
 
-                $timestamp = microtime(true);
                 // $this->logDebugTrackedProject("BEFORE listRows() | Table: {$table->getName()} | Batch: $batchSize | Timestamp: {$timestamp}");
 
-                $response = $this->reader->listRows($table, $queries);
-
-                $timestamp = microtime(true);
-                // $this->logDebugTrackedProject("AFTER listRows() | Table: {$table->getName()} | Rows: " . count($response) . " | Timestamp: $timestamp");
+                $response = $this->reader->listRows($table, $queries);
+                // $this->logDebugTrackedProject("AFTER listRows() | Table: {$table->getName()} | Rows: " . count($response) . " | Timestamp: " . microtime(true));
🧰 Tools
🪛 PHPMD (2.15.0)

993-993: Avoid unused local variables such as '$memUsage'. (undefined)

(UnusedLocalVariable)


1028-1028: Avoid unused local variables such as '$timestamp'. (undefined)

(UnusedLocalVariable)

🤖 Prompt for AI Agents
In `@src/Migration/Sources/Appwrite.php` around lines 991 - 1034, Remove the
unused local variables that PHPMD flags: delete the $memUsage assignment and the
two $timestamp assignments around the listRows call in the method that iterates
table rows (the block using $this->reader->listRows($table, $queries));
alternatively, if you want to keep those diagnostics, re-enable the commented
$this->logDebugTrackedProject(...) calls or guard them behind a debug flag so
the variables are actually used (refer to the logDebugTrackedProject call sites
and the $this->reader->listRows invocation when making the change).

Comment on lines 1072 to 1084
$row = self::getRecord($table->getDatabase()->getDatabaseName(), [
'id' => $id,
'table' => [
'id' => $table->getId(),
'name' => $table->getTableName(),
'rowSecurity' => $table->getRowSecurity(),
'permissions' => $table->getPermissions(),
'database' => [
'id' => $table->getDatabase()->getId(),
'name' => $table->getDatabase()->getDatabaseName(),
'type' => $table->getDatabase()->getType(),
'database' => $table->getDatabase()->getDatabase(),
]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Record type selection uses database name instead of database type.

Line 1072 calls getRecord($table->getDatabase()->getDatabaseName(), ...). getRecord() expects a database type (TYPE_DATABASE_DOCUMENTSDB, TYPE_DATABASE_VECTORDB, etc.), so passing the database name will cause Document DB/Vector DB records to be created as Row by default.

🔧 Suggested fix
-                    $row = self::getRecord($table->getDatabase()->getDatabaseName(), [
+                    $row = self::getRecord($table->getDatabase()->getType(), [
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$row = self::getRecord($table->getDatabase()->getDatabaseName(), [
'id' => $id,
'table' => [
'id' => $table->getId(),
'name' => $table->getTableName(),
'rowSecurity' => $table->getRowSecurity(),
'permissions' => $table->getPermissions(),
'database' => [
'id' => $table->getDatabase()->getId(),
'name' => $table->getDatabase()->getDatabaseName(),
'type' => $table->getDatabase()->getType(),
'database' => $table->getDatabase()->getDatabase(),
]
$row = self::getRecord($table->getDatabase()->getType(), [
'id' => $id,
'table' => [
'id' => $table->getId(),
'name' => $table->getTableName(),
'rowSecurity' => $table->getRowSecurity(),
'permissions' => $table->getPermissions(),
'database' => [
'id' => $table->getDatabase()->getId(),
'name' => $table->getDatabase()->getDatabaseName(),
'type' => $table->getDatabase()->getType(),
'database' => $table->getDatabase()->getDatabase(),
]
🤖 Prompt for AI Agents
In `@src/Migration/Sources/Appwrite.php` around lines 1072 - 1084, The code calls
getRecord with the database name instead of its type which makes record creation
default to Row; change the first argument of getRecord to use
$table->getDatabase()->getType() (instead of getDatabaseName()) so getRecord
receives the expected database type; update the call site in the Appwrite
migration code where getRecord(...) is invoked and ensure related metadata (the
rest of the associative array built from $table and $table->getDatabase())
remains unchanged.

Comment on lines 91 to 97
$databaseType = $database->getAttribute('type');
if (in_array($databaseType, [Resource::TYPE_DATABASE_LEGACY,Resource::TYPE_DATABASE_TABLESDB])) {
$databaseType = Resource::TYPE_DATABASE;
}

if (!Resource::isSupported([Resource::TYPE_ROW, Resource::TYPE_COLUMN, Resource::TYPE_INDEX], $resources)) {
continue;
}
$databaseSpecificResources = Resource::DATABASE_TYPE_RESOURCE_MAP[$databaseType];

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard against missing/unknown database type before indexing the resource map.

Line 91 uses $database->getAttribute('type') without a default and immediately indexes Resource::DATABASE_TYPE_RESOURCE_MAP[$databaseType]. If older DB metadata lacks type (or has an unexpected value), this will trigger an undefined index and break the report. Consider defaulting to Resource::TYPE_DATABASE when the value is missing or not in the map.

🔧 Suggested fix
-            $databaseType = $database->getAttribute('type');
-            if (in_array($databaseType, [Resource::TYPE_DATABASE_LEGACY,Resource::TYPE_DATABASE_TABLESDB])) {
-                $databaseType = Resource::TYPE_DATABASE;
-            }
-
-            $databaseSpecificResources = Resource::DATABASE_TYPE_RESOURCE_MAP[$databaseType];
+            $databaseType = $database->getAttribute('type', Resource::TYPE_DATABASE);
+            if (\in_array($databaseType, [Resource::TYPE_DATABASE_LEGACY, Resource::TYPE_DATABASE_TABLESDB], true)) {
+                $databaseType = Resource::TYPE_DATABASE;
+            }
+            if (!isset(Resource::DATABASE_TYPE_RESOURCE_MAP[$databaseType])) {
+                $databaseType = Resource::TYPE_DATABASE;
+            }
+
+            $databaseSpecificResources = Resource::DATABASE_TYPE_RESOURCE_MAP[$databaseType];
🤖 Prompt for AI Agents
In `@src/Migration/Sources/Appwrite/Reader/Database.php` around lines 91 - 97, The
code reads $database->getAttribute('type') and immediately indexes
Resource::DATABASE_TYPE_RESOURCE_MAP[$databaseType], which can throw an
undefined index for missing/unexpected types; update the logic in the block
around $databaseType and $databaseSpecificResources to default to
Resource::TYPE_DATABASE when getAttribute('type') returns null/empty or when
$databaseType is not a key in Resource::DATABASE_TYPE_RESOURCE_MAP (also keep
the existing normalization for Resource::TYPE_DATABASE_LEGACY and
Resource::TYPE_DATABASE_TABLESDB), e.g. compute $databaseType with a fallback
and guard the lookup with isset/array_key_exists before assigning
$databaseSpecificResources so it never indexes an unknown key.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/Migration/Destinations/Appwrite.php (1)

796-813: Normalized dates computed but not used.

Lines 796-797 normalize the dates, but lines 811-812 use the raw resource values instead. This inconsistency could cause date format issues.

Proposed fix
         $index = new UtopiaDocument([
             '$id' => ID::custom($database->getSequence() . '_' . $table->getSequence() . '_' . $resource->getKey()),
             'key' => $resource->getKey(),
             'status' => 'available', // processing, available, failed, deleting, stuck
             'databaseInternalId' => $database->getSequence(),
             'databaseId' => $database->getId(),
             'collectionInternalId' => $table->getSequence(),
             'collectionId' => $table->getId(),
             'type' => $resource->getType(),
             'attributes' => $resource->getColumns(),
             'lengths' => $lengths,
             'orders' => $resource->getOrders(),
-            '$createdAt' => $resource->getCreatedAt(),
-            '$updatedAt' => $resource->getUpdatedAt(),
+            '$createdAt' => $createdAt,
+            '$updatedAt' => $updatedAt,
         ]);
🤖 Fix all issues with AI agents
In `@src/Migration/Destinations/Appwrite.php`:
- Around line 82-84: The PHPDoc parameter name for the callable is incorrect: it
documents $getDatabasesDSN but the actual parameter is named $getDatabaseDSN;
update the PHPDoc in src/Migration/Destinations/Appwrite.php so the param
annotation matches the real parameter name ($getDatabaseDSN) (or rename the
function parameter to $getDatabasesDSN if you prefer consistency), ensuring the
callable signature stays callable(string $databaseType):string and references
the correct symbol ($getDatabaseDSN) used in the class/method.
- Around line 352-357: The code duplicates the normalizeDateTime calls for
$createdAt and $updatedAt in the Appwrite migration (duplicate lines call
normalizeDateTime twice); remove the second pair of duplicate calls so
$createdAt and $updatedAt are only set once (keep the first calls that use
normalizeDateTime($resource->getCreatedAt()) and
normalizeDateTime($resource->getUpdatedAt(), $createdAt)), ensuring the
$updatedAt uses the $createdAt value from the initial normalization.
- Around line 973-994: The foreach currently declares an unused $value when
iterating $this->rowBuffer rows; change the inner loop to iterate keys only so
$value is not declared (e.g. replace foreach ($row as $key => $value) with a
keys-only iteration such as foreach (array_keys($row->getArrayCopy()) as $key)),
keeping the surrounding logic that checks $table->getAttribute('attributes', [])
and calls $row->removeAttribute($key) intact; this removes the unused $value
while preserving the orphan-attribute removal behavior tied to
$dbForDatabases->getAdapter()->getSupportForAttributes().
- Around line 429-434: The code duplicates calls to $this->normalizeDateTime for
$createdAt and $updatedAt in the Appwrite migration class; remove the second
pair of duplicate lines so only the first calls remain (keep the first
assignments: $createdAt = $this->normalizeDateTime($resource->getCreatedAt());
and $updatedAt = $this->normalizeDateTime($resource->getUpdatedAt(),
$createdAt);), ensuring the variables are assigned once in the method in class
Appwrite (look for the normalizeDateTime calls around the existing assignments
to $createdAt and $updatedAt).
- Line 15: Remove the unused import Dom\Document from the Appwrite migration
file: locate the top-level use statement "use Dom\Document" in the Appwrite
class and delete it (or replace it with the correct import if it was intended to
be used), ensuring no references to Document remain in the class (check methods
in class Appwrite) so the file no longer contains the unused import.
♻️ Duplicate comments (1)
src/Migration/Destinations/Appwrite.php (1)

471-476: Clarify DocumentsDB guard behavior and message.

The guard blocks both Column and Attribute resources for DocumentsDB, but the status message only mentions "Columns". Consider updating the message to accurately reflect the intended behavior:

Proposed fix
         if ($resource->getTable()->getDatabase()->getType() === Resource::TYPE_DATABASE_DOCUMENTSDB) {
-            $resource->setStatus(Resource::STATUS_SKIPPED, 'Columns not supported for DocumentsDB');
+            $resource->setStatus(Resource::STATUS_SKIPPED, 'Schema fields not supported for DocumentsDB');
             return false;
         }
🧹 Nitpick comments (1)
src/Migration/Destinations/Appwrite.php (1)

1534-1537: Unused variable $columnSize.

The variable $columnSize is extracted but never used. Either remove it or add a comment indicating it's reserved for future use.

Proposed fix (remove unused variable)
             $columnStatus = $oldColumns[$columnIndex]['status'];
             $columnType = $oldColumns[$columnIndex]['type'];
-            $columnSize = $oldColumns[$columnIndex]['size'];
             $columnArray = $oldColumns[$columnIndex]['array'] ?? false;

use Appwrite\Services\Storage;
use Appwrite\Services\Teams;
use Appwrite\Services\Users;
use Dom\Document;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Unused import detected.

Dom\Document is imported but not used anywhere in the file. This appears to be a leftover from refactoring.

Proposed fix
-use Dom\Document;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
use Dom\Document;
🤖 Prompt for AI Agents
In `@src/Migration/Destinations/Appwrite.php` at line 15, Remove the unused import
Dom\Document from the Appwrite migration file: locate the top-level use
statement "use Dom\Document" in the Appwrite class and delete it (or replace it
with the correct import if it was intended to be used), ensuring no references
to Document remain in the class (check methods in class Appwrite) so the file no
longer contains the unused import.

Comment on lines 82 to 84
* @param UtopiaDatabase $dbForProject
* @param callable(UtopiaDocument $database):UtopiaDatabase $getDatabasesDB
* @param callable(string $databaseType):string $getDatabasesDSN
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

PHPDoc parameter name mismatch.

The PHPDoc shows $getDatabasesDSN but the actual parameter is $getDatabaseDSN (without the 's').

Proposed fix
-     * `@param` callable(string $databaseType):string $getDatabasesDSN
+     * `@param` callable(string $databaseType):string $getDatabaseDSN
🤖 Prompt for AI Agents
In `@src/Migration/Destinations/Appwrite.php` around lines 82 - 84, The PHPDoc
parameter name for the callable is incorrect: it documents $getDatabasesDSN but
the actual parameter is named $getDatabaseDSN; update the PHPDoc in
src/Migration/Destinations/Appwrite.php so the param annotation matches the real
parameter name ($getDatabaseDSN) (or rename the function parameter to
$getDatabasesDSN if you prefer consistency), ensuring the callable signature
stays callable(string $databaseType):string and references the correct symbol
($getDatabaseDSN) used in the class/method.

Comment on lines 352 to 357
$createdAt = $this->normalizeDateTime($resource->getCreatedAt());
$updatedAt = $this->normalizeDateTime($resource->getUpdatedAt(), $createdAt);

$database = $this->database->createDocument('databases', new UtopiaDocument([
$createdAt = $this->normalizeDateTime($resource->getCreatedAt());
$updatedAt = $this->normalizeDateTime($resource->getUpdatedAt(), $createdAt);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Duplicate normalizeDateTime calls.

Lines 355-356 duplicate the exact same normalization already performed on lines 352-353. This appears to be a copy-paste error.

Proposed fix
         $createdAt = $this->normalizeDateTime($resource->getCreatedAt());
         $updatedAt = $this->normalizeDateTime($resource->getUpdatedAt(), $createdAt);

-        $createdAt = $this->normalizeDateTime($resource->getCreatedAt());
-        $updatedAt = $this->normalizeDateTime($resource->getUpdatedAt(), $createdAt);
-
         $database = $this->dbForProject->createDocument('databases', new UtopiaDocument([
🤖 Prompt for AI Agents
In `@src/Migration/Destinations/Appwrite.php` around lines 352 - 357, The code
duplicates the normalizeDateTime calls for $createdAt and $updatedAt in the
Appwrite migration (duplicate lines call normalizeDateTime twice); remove the
second pair of duplicate calls so $createdAt and $updatedAt are only set once
(keep the first calls that use normalizeDateTime($resource->getCreatedAt()) and
normalizeDateTime($resource->getUpdatedAt(), $createdAt)), ensuring the
$updatedAt uses the $createdAt value from the initial normalization.

Comment on lines 429 to 434
$createdAt = $this->normalizeDateTime($resource->getCreatedAt());
$updatedAt = $this->normalizeDateTime($resource->getUpdatedAt(), $createdAt);

$table = $this->database->createDocument('database_' . $database->getSequence(), new UtopiaDocument([
$createdAt = $this->normalizeDateTime($resource->getCreatedAt());
$updatedAt = $this->normalizeDateTime($resource->getUpdatedAt(), $createdAt);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Duplicate normalizeDateTime calls.

Lines 432-433 duplicate the normalization already performed on lines 429-430.

Proposed fix
         $createdAt = $this->normalizeDateTime($resource->getCreatedAt());
         $updatedAt = $this->normalizeDateTime($resource->getUpdatedAt(), $createdAt);

-        $createdAt = $this->normalizeDateTime($resource->getCreatedAt());
-        $updatedAt = $this->normalizeDateTime($resource->getUpdatedAt(), $createdAt);
-
         $dbForDatabases = ($this->getDatabasesDB)($database);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$createdAt = $this->normalizeDateTime($resource->getCreatedAt());
$updatedAt = $this->normalizeDateTime($resource->getUpdatedAt(), $createdAt);
$table = $this->database->createDocument('database_' . $database->getSequence(), new UtopiaDocument([
$createdAt = $this->normalizeDateTime($resource->getCreatedAt());
$updatedAt = $this->normalizeDateTime($resource->getUpdatedAt(), $createdAt);
$createdAt = $this->normalizeDateTime($resource->getCreatedAt());
$updatedAt = $this->normalizeDateTime($resource->getUpdatedAt(), $createdAt);
$dbForDatabases = ($this->getDatabasesDB)($database);
🤖 Prompt for AI Agents
In `@src/Migration/Destinations/Appwrite.php` around lines 429 - 434, The code
duplicates calls to $this->normalizeDateTime for $createdAt and $updatedAt in
the Appwrite migration class; remove the second pair of duplicate lines so only
the first calls remain (keep the first assignments: $createdAt =
$this->normalizeDateTime($resource->getCreatedAt()); and $updatedAt =
$this->normalizeDateTime($resource->getUpdatedAt(), $createdAt);), ensuring the
variables are assigned once in the method in class Appwrite (look for the
normalizeDateTime calls around the existing assignments to $createdAt and
$updatedAt).

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/Migration/Destinations/Appwrite.php (2)

796-813: Use normalized timestamps for index metadata.

$createdAt/$updatedAt are computed but not used. If the raw resource values aren’t normalized, index creation can fail validation.

✅ Fix
-            '$createdAt' => $resource->getCreatedAt(),
-            '$updatedAt' => $resource->getUpdatedAt(),
+            '$createdAt' => $createdAt,
+            '$updatedAt' => $updatedAt,

570-598: Use the target database adapter for attribute validation.

The checkAttribute() call validates against $this->dbForProject, which is the project metadata store, not the target database adapter. Since the attribute definition includes adapter-specific fields (type, format, formatOptions, filters), validation should occur against $dbForDatabases to ensure compatibility with the target adapter's capabilities (e.g., vector support, spatial rules). Additionally, $dbForDatabases is already initialized on line 572 but remains unused, suggesting this is the intended validation point.

🔧 Proposed fix
-            $this->dbForProject->checkAttribute($table, $column);
+            $dbForDatabases->checkAttribute($table, $column);
🤖 Fix all issues with AI agents
In `@src/Migration/Destinations/Appwrite.php`:
- Around line 1539-1542: Remove the unused variable assignment $columnSize from
the code that reads column metadata (the block that assigns $columnStatus,
$columnType, $columnSize, $columnArray from $oldColumns[$columnIndex]);
specifically delete the line assigning $columnSize (or, if size is actually
needed, use $columnSize where appropriate), leaving only $columnStatus,
$columnType and $columnArray assignments to resolve the PHPMD unused-variable
warning.

Comment on lines 1539 to 1542
$columnStatus = $oldColumns[$columnIndex]['status'];
$columnType = $oldColumns[$columnIndex]['type'];
$columnSize = $oldColumns[$columnIndex]['size'];
$columnArray = $oldColumns[$columnIndex]['array'] ?? false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove unused $columnSize.

$columnSize is assigned but never used (PHPMD).

🧹 Cleanup
-            $columnSize = $oldColumns[$columnIndex]['size'];
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$columnStatus = $oldColumns[$columnIndex]['status'];
$columnType = $oldColumns[$columnIndex]['type'];
$columnSize = $oldColumns[$columnIndex]['size'];
$columnArray = $oldColumns[$columnIndex]['array'] ?? false;
$columnStatus = $oldColumns[$columnIndex]['status'];
$columnType = $oldColumns[$columnIndex]['type'];
$columnArray = $oldColumns[$columnIndex]['array'] ?? false;
🧰 Tools
🪛 PHPMD (2.15.0)

1541-1541: Avoid unused local variables such as '$columnSize'. (undefined)

(UnusedLocalVariable)

🤖 Prompt for AI Agents
In `@src/Migration/Destinations/Appwrite.php` around lines 1539 - 1542, Remove the
unused variable assignment $columnSize from the code that reads column metadata
(the block that assigns $columnStatus, $columnType, $columnSize, $columnArray
from $oldColumns[$columnIndex]); specifically delete the line assigning
$columnSize (or, if size is actually needed, use $columnSize where appropriate),
leaving only $columnStatus, $columnType and $columnArray assignments to resolve
the PHPMD unused-variable warning.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@src/Migration/Destinations/Appwrite.php`:
- Around line 811-812: The index document is using raw dates
($resource->getCreatedAt() / $resource->getUpdatedAt()) instead of the
previously normalized $createdAt and $updatedAt values; update the array entries
(where the index document is built in Appwrite.php) to use the normalized
$createdAt and $updatedAt variables instead of calling $resource->getCreatedAt()
and $resource->getUpdatedAt() so the stored datetimes match other create*
methods and maintain valid normalized formatting.

In `@src/Migration/Sources/Appwrite.php`:
- Around line 91-94: The PHPDoc for the property $getDatabasesDB uses a
non-existent type and invalid nullable syntax; update the docblock to reference
the correct fully-qualified document type and valid nullable form, e.g. change
the annotation to: `@var` callable(\Utopia\Database\Document|null $database):
\Utopia\Database\Document (or the correct fully-qualified return type used in
this project) so the parameter type is \Utopia\Database\Document|null and the
callable return type is fully qualified.
- Around line 644-665: The error uses the generic $resourceKey when constructing
the Exception inside the $handleExportEntityScopedResources closure; change it
to use the resolved resource type from the map (e.g. $entityResource['type'] or
the appropriate key in Resource::ENTITY_TYPE_RESOURCE_MAP) so the Exception
contains the specific resource name; update the Exception call inside the catch
block (where addError(...) is invoked) to pass the resolved resource type (from
$entityResource) instead of $resourceKey while keeping other fields
(Transfer::GROUP_DATABASES, message, code, previous) unchanged.
- Around line 893-901: The routing for column/attribute construction is
inconsistent: getEntity() returns Collection for both TYPE_DATABASE_VECTORDB and
TYPE_DATABASE_DOCUMENTSDB but the match only calls getAttribute(...) for
TYPE_DATABASE_VECTORDB and falls back to getColumn(...) for DocumentsDB; update
the match in the block that builds $col so DocumentsDB also uses
getAttribute($table, $column) (or otherwise branch by inspecting getEntity()
type) instead of defaulting to getColumn($table, $column) so Collection
instances produce Attribute objects consistently; modify the match to include
Resource::TYPE_DATABASE_DOCUMENTSDB to call getAttribute, keeping getColumn as
the default for Table-backed schemas.
🧹 Nitpick comments (3)
src/Migration/Sources/Appwrite.php (3)

1574-1582: DocumentsDB and VectorDB cases are identical — consolidate.

Both TYPE_DATABASE_DOCUMENTSDB and TYPE_DATABASE_VECTORDB return Collection::fromArray($entity). Merge them into a single arm.

Proposed fix
     public static function getEntity(string $databaseType, array $entity): Resource
     {
         switch ($databaseType) {
             case Resource::TYPE_DATABASE_DOCUMENTSDB:
-                return Collection::fromArray($entity);
             case Resource::TYPE_DATABASE_VECTORDB:
                 return Collection::fromArray($entity);
             default:
                 return Table::fromArray($entity);
         }
     }

1613-1623: Same consolidation opportunity in getRecord().

Both DocumentsDB and VectorDB cases return Document::fromArray($record).

Proposed fix
     public static function getRecord(string $databaseType, array $record): Resource
     {
         switch ($databaseType) {
             case Resource::TYPE_DATABASE_DOCUMENTSDB:
-                return Document::fromArray($record);
             case Resource::TYPE_DATABASE_VECTORDB:
                 return Document::fromArray($record);
             default:
                 return Row::fromArray($record);
         }
     }

1625-1788: getColumn() and getAttribute() are near-identical — consider extracting shared logic.

These two large factory methods (~160 lines each) have the same structure and match arms, differing only in Column vs Attribute types and Table vs Collection parameter types. This is a significant amount of duplicated logic that could become a maintenance burden when adding new types.

Consider a shared private helper that takes a factory-class map or uses a common interface.

Also applies to: 1790-1952

Comment on lines 811 to 812
'$createdAt' => $resource->getCreatedAt(),
'$updatedAt' => $resource->getUpdatedAt(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Index document uses raw dates instead of normalized values.

Lines 796–797 normalize $createdAt and $updatedAt, but lines 811–812 bypass them and use $resource->getCreatedAt() / $resource->getUpdatedAt() directly. This is inconsistent with all other create* methods and could produce invalid datetime formats in the stored document.

Proposed fix
-            '$createdAt' => $resource->getCreatedAt(),
-            '$updatedAt' => $resource->getUpdatedAt(),
+            '$createdAt' => $createdAt,
+            '$updatedAt' => $updatedAt,
🤖 Prompt for AI Agents
In `@src/Migration/Destinations/Appwrite.php` around lines 811 - 812, The index
document is using raw dates ($resource->getCreatedAt() /
$resource->getUpdatedAt()) instead of the previously normalized $createdAt and
$updatedAt values; update the array entries (where the index document is built
in Appwrite.php) to use the normalized $createdAt and $updatedAt variables
instead of calling $resource->getCreatedAt() and $resource->getUpdatedAt() so
the stored datetimes match other create* methods and maintain valid normalized
formatting.

Comment on lines +91 to +94
/**
* @var callable(UtopiaDocument $database|null): UtopiaDatabase
*/
protected mixed $getDatabasesDB;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

PHPDoc callable signature references non-existent type UtopiaDocument.

Line 92: callable(UtopiaDocument $database|null)UtopiaDocument is not imported in this file. This should likely be \Utopia\Database\Document or simply documented differently. Also the |null syntax is not valid PHPDoc for a callable parameter.

🤖 Prompt for AI Agents
In `@src/Migration/Sources/Appwrite.php` around lines 91 - 94, The PHPDoc for the
property $getDatabasesDB uses a non-existent type and invalid nullable syntax;
update the docblock to reference the correct fully-qualified document type and
valid nullable form, e.g. change the annotation to: `@var`
callable(\Utopia\Database\Document|null $database): \Utopia\Database\Document
(or the correct fully-qualified return type used in this project) so the
parameter type is \Utopia\Database\Document|null and the callable return type is
fully qualified.

Comment on lines 644 to 665
$handleExportEntityScopedResources = function (string $resourceKey, callable $callback) use ($resources) {
foreach (Resource::ENTITY_TYPE_RESOURCE_MAP as $entityKey => $entityResource) {
try {
if (\in_array($entityResource[$resourceKey], $resources)) {
$callback($entityKey, $entityResource);
}
} catch (\Throwable $e) {
$this->addError(
new Exception(
$resourceKey,
Transfer::GROUP_DATABASES,
message: $e->getMessage(),
code: $e->getCode(),
previous: $e
)
);

return;
}
return false;
}
}
return true;
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Error reporting uses generic key instead of specific resource type.

In the $handleExportEntityScopedResources closure, the Exception at line 653 uses $resourceKey (e.g., 'field', 'index', 'record') as the resource name. This should be the resolved resource type from the map to provide meaningful error context.

Proposed fix
                 } catch (\Throwable $e) {
                     $this->addError(
                         new Exception(
-                            $resourceKey,
+                            $entityResource[$resourceKey],
                             Transfer::GROUP_DATABASES,
                             message: $e->getMessage(),
                             code: $e->getCode(),
                             previous: $e
                         )
                     );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$handleExportEntityScopedResources = function (string $resourceKey, callable $callback) use ($resources) {
foreach (Resource::ENTITY_TYPE_RESOURCE_MAP as $entityKey => $entityResource) {
try {
if (\in_array($entityResource[$resourceKey], $resources)) {
$callback($entityKey, $entityResource);
}
} catch (\Throwable $e) {
$this->addError(
new Exception(
$resourceKey,
Transfer::GROUP_DATABASES,
message: $e->getMessage(),
code: $e->getCode(),
previous: $e
)
);
return;
}
return false;
}
}
return true;
};
$handleExportEntityScopedResources = function (string $resourceKey, callable $callback) use ($resources) {
foreach (Resource::ENTITY_TYPE_RESOURCE_MAP as $entityKey => $entityResource) {
try {
if (\in_array($entityResource[$resourceKey], $resources)) {
$callback($entityKey, $entityResource);
}
} catch (\Throwable $e) {
$this->addError(
new Exception(
$entityResource[$resourceKey],
Transfer::GROUP_DATABASES,
message: $e->getMessage(),
code: $e->getCode(),
previous: $e
)
);
return false;
}
}
return true;
};
🤖 Prompt for AI Agents
In `@src/Migration/Sources/Appwrite.php` around lines 644 - 665, The error uses
the generic $resourceKey when constructing the Exception inside the
$handleExportEntityScopedResources closure; change it to use the resolved
resource type from the map (e.g. $entityResource['type'] or the appropriate key
in Resource::ENTITY_TYPE_RESOURCE_MAP) so the Exception contains the specific
resource name; update the Exception call inside the catch block (where
addError(...) is invoked) to pass the resolved resource type (from
$entityResource) instead of $resourceKey while keeping other fields
(Transfer::GROUP_DATABASES, message, code, previous) unchanged.

Comment on lines 893 to 901
}

switch ($column['type']) {
case Column::TYPE_STRING:
$col = match ($column['format'] ?? '') {
Column::TYPE_EMAIL => new Email(
$column['key'],
$table,
required: $column['required'],
default: $column['default'],
array: $column['array'],
size: $column['size'] ?? 254,
createdAt: $column['$createdAt'] ?? '',
updatedAt: $column['$updatedAt'] ?? '',
),
Column::TYPE_ENUM => new Enum(
$column['key'],
$table,
elements: $column['elements'],
required: $column['required'],
default: $column['default'],
array: $column['array'],
size: $column['size'] ?? UtopiaDatabase::LENGTH_KEY,
createdAt: $column['$createdAt'] ?? '',
updatedAt: $column['$updatedAt'] ?? '',
),
Column::TYPE_URL => new URL(
$column['key'],
$table,
required: $column['required'],
default: $column['default'],
array: $column['array'],
size: $column['size'] ?? 2000,
createdAt: $column['$createdAt'] ?? '',
updatedAt: $column['$updatedAt'] ?? '',
),
Column::TYPE_IP => new IP(
$column['key'],
$table,
required: $column['required'],
default: $column['default'],
array: $column['array'],
size: $column['size'] ?? 39,
createdAt: $column['$createdAt'] ?? '',
updatedAt: $column['$updatedAt'] ?? '',
),
default => new Text(
$column['key'],
$table,
required: $column['required'],
default: $column['default'],
array: $column['array'],
size: $column['size'] ?? 0,
createdAt: $column['$createdAt'] ?? '',
updatedAt: $column['$updatedAt'] ?? '',
),
};

break;
case Column::TYPE_BOOLEAN:
$col = new Boolean(
$column['key'],
$table,
required: $column['required'],
default: $column['default'],
array: $column['array'],
createdAt: $column['$createdAt'] ?? '',
updatedAt: $column['$updatedAt'] ?? '',
);
break;
case Column::TYPE_INTEGER:
$col = new Integer(
$column['key'],
$table,
required: $column['required'],
default: $column['default'],
array: $column['array'],
min: $column['min'] ?? null,
max: $column['max'] ?? null,
createdAt: $column['$createdAt'] ?? '',
updatedAt: $column['$updatedAt'] ?? '',
);
break;
case Column::TYPE_FLOAT:
$col = new Decimal(
$column['key'],
$table,
required: $column['required'],
default: $column['default'],
array: $column['array'],
min: $column['min'] ?? null,
max: $column['max'] ?? null,
createdAt: $column['$createdAt'] ?? '',
updatedAt: $column['$updatedAt'] ?? '',
);
break;
case Column::TYPE_RELATIONSHIP:
$col = new Relationship(
$column['key'],
$table,
relatedTable: $column['relatedTable'] ?? $column['relatedCollection'],
relationType: $column['relationType'],
twoWay: $column['twoWay'],
twoWayKey: $column['twoWayKey'],
onDelete: $column['onDelete'],
side: $column['side'],
createdAt: $column['$createdAt'] ?? '',
updatedAt: $column['$updatedAt'] ?? '',
);
break;
case Column::TYPE_DATETIME:
$col = new DateTime(
$column['key'],
$table,
required: $column['required'],
default: $column['default'],
array: $column['array'],
createdAt: $column['$createdAt'] ?? '',
updatedAt: $column['$updatedAt'] ?? '',
);
break;
case Column::TYPE_POINT:
$col = new Point(
$column['key'],
$table,
required: $column['required'],
default: $column['default'],
createdAt: $column['$createdAt'] ?? '',
updatedAt: $column['$updatedAt'] ?? '',
);
break;
case Column::TYPE_LINE:
$col = new Line(
$column['key'],
$table,
required: $column['required'],
default: $column['default'],
createdAt: $column['$createdAt'] ?? '',
updatedAt: $column['$updatedAt'] ?? '',
);
break;
case Column::TYPE_POLYGON:
$col = new Polygon(
$column['key'],
$table,
required: $column['required'],
default: $column['default'],
createdAt: $column['$createdAt'] ?? '',
updatedAt: $column['$updatedAt'] ?? '',
);
break;
}

if (!isset($col)) {
throw new Exception(
resourceName: Resource::TYPE_COLUMN,
resourceGroup: Transfer::GROUP_DATABASES,
resourceId: $column['$id'],
message: 'Unknown column type: ' . $column['type']
);
}
/** @var Table $table */
$col = match($table->getDatabase()->getType()) {
Resource::TYPE_DATABASE_VECTORDB => self::getAttribute($table, $column),
default => self::getColumn($table, $column),
};

$columns[] = $col;
Copy link
Contributor

@coderabbitai coderabbitai bot Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

# Check the actual code at lines 893-901
echo "=== Code at lines 893-901 ==="
sed -n '893,901p' src/Migration/Sources/Appwrite.php

# Check Collection and Table class definitions
echo -e "\n=== Collection class definition ==="
rg -n "class Collection" --type php -A 5

echo -e "\n=== Table class definition ==="
rg -n "class Table" --type php -A 5

# Check getAttribute and getColumn signatures
echo -e "\n=== getAttribute method ==="
rg -n "public static function getAttribute" --type php -A 3

echo -e "\n=== getColumn method ==="
rg -n "public static function getColumn" --type php -A 3

# Check getEntity method to see what it returns for different DB types
echo -e "\n=== getEntity method ==="
rg -n "private function getEntity" --type php -B 2 -A 10

# Check the database type constants
echo -e "\n=== Database type constants ==="
rg -n "TYPE_DATABASE_" --type php

Repository: utopia-php/migration

Length of output: 6202


🏁 Script executed:

#!/bin/bash

# Get broader context around the match statement
echo "=== Context around lines 880-910 ==="
sed -n '880,910p' src/Migration/Sources/Appwrite.php

# Also check if there's a method that processes columns and what it gets from getEntity
echo -e "\n=== Context around line 893-901 with method signature ==="
sed -n '860,920p' src/Migration/Sources/Appwrite.php

# Check what getEntity returns for DocumentsDB and VectorDB
echo -e "\n=== Look for where entities are fetched ==="
rg -n "getEntity|->get\(" src/Migration/Sources/Appwrite.php --type php -B 2 -A 2 | head -60

Repository: utopia-php/migration

Length of output: 5335


🏁 Script executed:

#!/bin/bash

# Find the getEntity method
echo "=== getEntity method definition ==="
rg -n "public static function getEntity" --type php -A 15

# Also check if there are any calls to getEntity with database type routing
echo -e "\n=== getEntity calls with database type context ==="
rg -n "getEntity.*TYPE_DATABASE" --type php -B 3 -A 3

Repository: utopia-php/migration

Length of output: 1313


🏁 Script executed:

#!/bin/bash

# Get full getAttribute implementation
echo "=== getAttribute implementation ==="
sed -n '1790,1850p' src/Migration/Sources/Appwrite.php

# Get full getColumn implementation
echo -e "\n=== getColumn implementation ==="
sed -n '1625,1685p' src/Migration/Sources/Appwrite.php

# Check if getAttribute is used elsewhere with Collection
echo -e "\n=== getAttribute usage in codebase ==="
rg -n "getAttribute\(" src/Migration/Sources/Appwrite.php --type php | head -20

Repository: utopia-php/migration

Length of output: 5895


The type mismatch claim is incorrect — no TypeError will occur at runtime.

Collection extends Table, so passing a Collection instance to getColumn(Table $table, ...) is valid. However, there is a semantic routing inconsistency: both TYPE_DATABASE_DOCUMENTSDB and TYPE_DATABASE_VECTORDB return Collection entities from getEntity() (lines 1576–1578), yet only VectorDB routes to getAttribute(), which is designed for Collection. DocumentsDB falls through to the default case, calling getColumn(Table, ...) instead, creating Column objects for data that semantically represents Attributes. Consider aligning the routing:

Suggested fix
                 /** `@var` Table $table */
                 $col = match($table->getDatabase()->getType()) {
                     Resource::TYPE_DATABASE_VECTORDB => self::getAttribute($table, $column),
+                    Resource::TYPE_DATABASE_DOCUMENTSDB => self::getAttribute($table, $column),
                     default => self::getColumn($table, $column),
                 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
}
switch ($column['type']) {
case Column::TYPE_STRING:
$col = match ($column['format'] ?? '') {
Column::TYPE_EMAIL => new Email(
$column['key'],
$table,
required: $column['required'],
default: $column['default'],
array: $column['array'],
size: $column['size'] ?? 254,
createdAt: $column['$createdAt'] ?? '',
updatedAt: $column['$updatedAt'] ?? '',
),
Column::TYPE_ENUM => new Enum(
$column['key'],
$table,
elements: $column['elements'],
required: $column['required'],
default: $column['default'],
array: $column['array'],
size: $column['size'] ?? UtopiaDatabase::LENGTH_KEY,
createdAt: $column['$createdAt'] ?? '',
updatedAt: $column['$updatedAt'] ?? '',
),
Column::TYPE_URL => new URL(
$column['key'],
$table,
required: $column['required'],
default: $column['default'],
array: $column['array'],
size: $column['size'] ?? 2000,
createdAt: $column['$createdAt'] ?? '',
updatedAt: $column['$updatedAt'] ?? '',
),
Column::TYPE_IP => new IP(
$column['key'],
$table,
required: $column['required'],
default: $column['default'],
array: $column['array'],
size: $column['size'] ?? 39,
createdAt: $column['$createdAt'] ?? '',
updatedAt: $column['$updatedAt'] ?? '',
),
default => new Text(
$column['key'],
$table,
required: $column['required'],
default: $column['default'],
array: $column['array'],
size: $column['size'] ?? 0,
createdAt: $column['$createdAt'] ?? '',
updatedAt: $column['$updatedAt'] ?? '',
),
};
break;
case Column::TYPE_BOOLEAN:
$col = new Boolean(
$column['key'],
$table,
required: $column['required'],
default: $column['default'],
array: $column['array'],
createdAt: $column['$createdAt'] ?? '',
updatedAt: $column['$updatedAt'] ?? '',
);
break;
case Column::TYPE_INTEGER:
$col = new Integer(
$column['key'],
$table,
required: $column['required'],
default: $column['default'],
array: $column['array'],
min: $column['min'] ?? null,
max: $column['max'] ?? null,
createdAt: $column['$createdAt'] ?? '',
updatedAt: $column['$updatedAt'] ?? '',
);
break;
case Column::TYPE_FLOAT:
$col = new Decimal(
$column['key'],
$table,
required: $column['required'],
default: $column['default'],
array: $column['array'],
min: $column['min'] ?? null,
max: $column['max'] ?? null,
createdAt: $column['$createdAt'] ?? '',
updatedAt: $column['$updatedAt'] ?? '',
);
break;
case Column::TYPE_RELATIONSHIP:
$col = new Relationship(
$column['key'],
$table,
relatedTable: $column['relatedTable'] ?? $column['relatedCollection'],
relationType: $column['relationType'],
twoWay: $column['twoWay'],
twoWayKey: $column['twoWayKey'],
onDelete: $column['onDelete'],
side: $column['side'],
createdAt: $column['$createdAt'] ?? '',
updatedAt: $column['$updatedAt'] ?? '',
);
break;
case Column::TYPE_DATETIME:
$col = new DateTime(
$column['key'],
$table,
required: $column['required'],
default: $column['default'],
array: $column['array'],
createdAt: $column['$createdAt'] ?? '',
updatedAt: $column['$updatedAt'] ?? '',
);
break;
case Column::TYPE_POINT:
$col = new Point(
$column['key'],
$table,
required: $column['required'],
default: $column['default'],
createdAt: $column['$createdAt'] ?? '',
updatedAt: $column['$updatedAt'] ?? '',
);
break;
case Column::TYPE_LINE:
$col = new Line(
$column['key'],
$table,
required: $column['required'],
default: $column['default'],
createdAt: $column['$createdAt'] ?? '',
updatedAt: $column['$updatedAt'] ?? '',
);
break;
case Column::TYPE_POLYGON:
$col = new Polygon(
$column['key'],
$table,
required: $column['required'],
default: $column['default'],
createdAt: $column['$createdAt'] ?? '',
updatedAt: $column['$updatedAt'] ?? '',
);
break;
}
if (!isset($col)) {
throw new Exception(
resourceName: Resource::TYPE_COLUMN,
resourceGroup: Transfer::GROUP_DATABASES,
resourceId: $column['$id'],
message: 'Unknown column type: ' . $column['type']
);
}
/** @var Table $table */
$col = match($table->getDatabase()->getType()) {
Resource::TYPE_DATABASE_VECTORDB => self::getAttribute($table, $column),
default => self::getColumn($table, $column),
};
$columns[] = $col;
/** `@var` Table $table */
$col = match($table->getDatabase()->getType()) {
Resource::TYPE_DATABASE_VECTORDB => self::getAttribute($table, $column),
Resource::TYPE_DATABASE_DOCUMENTSDB => self::getAttribute($table, $column),
default => self::getColumn($table, $column),
};
$columns[] = $col;
🤖 Prompt for AI Agents
In `@src/Migration/Sources/Appwrite.php` around lines 893 - 901, The routing for
column/attribute construction is inconsistent: getEntity() returns Collection
for both TYPE_DATABASE_VECTORDB and TYPE_DATABASE_DOCUMENTSDB but the match only
calls getAttribute(...) for TYPE_DATABASE_VECTORDB and falls back to
getColumn(...) for DocumentsDB; update the match in the block that builds $col
so DocumentsDB also uses getAttribute($table, $column) (or otherwise branch by
inspecting getEntity() type) instead of defaulting to getColumn($table, $column)
so Collection instances produce Attribute objects consistently; modify the match
to include Resource::TYPE_DATABASE_DOCUMENTSDB to call getAttribute, keeping
getColumn as the default for Table-backed schemas.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so documentsdb will not be present here as there is no attribute support in documentsdb

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check for DocumentsDB attribute support references
echo "=== DocumentsDB class and attribute-related code ==="
rg -n "DocumentsDB" --type php -B 2 -A 5

echo -e "\n=== Check exportFields method and its callers ==="
rg -n "exportFields|handleExportEntityScopedResources" --type php -B 3 -A 3

echo -e "\n=== Check DATABASE_TYPE_RESOURCE_MAP for field mappings ==="
rg -n "DATABASE_TYPE_RESOURCE_MAP" --type php -B 2 -A 10

echo -e "\n=== Check if DocumentsDB uses attributes or columns ==="
rg -n "TYPE_DATABASE_DOCUMENTSDB.*attribute|TYPE_DATABASE_DOCUMENTSDB.*column" --type php -i

Length of output: 12707


@ArnabChatterjee20k, you're absolutely right! I found the evidence in the codebase:

  1. In src/Migration/Resource.php (lines 108-109), there's a comment on the DocumentsDB field mapping:

    // HACK: not required in documentsdb but adding it for consistency in the db reader(not gonna impact)
    'field' => self::TYPE_ATTRIBUTE,
  2. In src/Migration/Destinations/Appwrite.php (lines 473-474), DocumentsDB explicitly skips columns:

    if ($resource->getTable()->getDatabase()->getType() === Resource::TYPE_DATABASE_DOCUMENTSDB) {
        $resource->setStatus(Resource::STATUS_SKIPPED, 'Columns not supported for DocumentsDB');

Since DocumentsDB doesn't support attributes/columns, DocumentsDB collections won't have any attributes to export, so they'll never reach the exportFields() code path. The match statement correctly only needs to handle VectorDB → getAttribute() routing, with the default handling regular Table → getColumn().

CodeRabbit's suggestion to add TYPE_DATABASE_DOCUMENTSDB to the match was based on structural symmetry but isn't needed given the actual runtime behavior. Thanks for the clarification!


🧬 Code Graph Analysis Results
  • src/Migration/Sources/Appwrite/Reader/API.php

    • Summary: report(resources, report, resourceIds)
      Description: Aggregates counts for Appwrite resources (databases, tables/collections, rows/documents, columns/attributes, indexes) based on the provided resources filter and optional resourceIds. Iterates databases and their tables to accumulate totals for each resource type. Returns null on success; throws if issues occur. Parameters:

      • resources: array of resource types to report on.
      • report: array passed by reference to accumulate counts per resource type.
      • resourceIds: optional map of resource type => list of IDs to filter on.
        Returns: mixed (typically null on completion, or throws on error).
        Exceptions/Errors: throws if there are errors while querying the Appwrite API or database, including permission/scope errors.
    • File: src/Migration/Sources/Appwrite/Reader/API.php

    • Snippet location: report method (high-level behavior described above).

    • Summary: listDatabases(queries)
      Description: Returns an array of databases for the API reader, applying optional query constraints.
      Returns: array of databases.

    • File: src/Migration/Sources/Appwrite/Reader/API.php

    • Snippet location: listDatabases(array $queries = []): array

    • Summary: listTables(Database $resource, queries)
      Description: Given a Database resource, returns the list of its tables/collections applying provided queries.

    • File: src/Migration/Sources/Appwrite/Reader/API.php

    • Snippet location: listTables(Database $resource, array $queries = []): array

    • Summary: listColumns(Table $resource, queries)
      Description: Given a Table/Collection resource, returns its columns/attributes applying provided queries.

    • File: src/Migration/Sources/Appwrite/Reader/API.php

    • Snippet location: listColumns(Table $resource, array $queries = []): array

    • Summary: listIndexes(Table $resource, queries)
      Description: Given a Table/Collection resource, returns its indexes applying provided queries.

    • File: src/Migration/Sources/Appwrite/Reader/API.php

    • Snippet location: listIndexes(Table $resource, array $queries = []): array

    • Summary: listRows(Table $resource, queries) and getRow(Table $resource, rowId, queries)
      Description: Retrieve rows/documents for a table and a single row by ID, respectively, using provided queries.

    • File: src/Migration/Sources/Appwrite/Reader/API.php

    • Snippet location: listRows(Table $resource, array $queries = []): array

    • Snippet location: getRow(Table $resource, string $rowId, array $queries = []): array

    • Summary: querySelect(columns), queryEqual(column, values), queryCursorAfter(resource|Resource), queryLimit(limit)
      Description: Helpers to build query objects for selecting columns, equality filters, cursorAfter semantics, and limit.

    • File: src/Migration/Sources/Appwrite/Reader/API.php

    • Snippet location: querySelect(array $columns): mixed

    • Snippet location: queryEqual(string $column, array $values): mixed

    • Snippet location: queryCursorAfter(Resource|string $resource): mixed

    • Snippet location: queryLimit(int $limit): mixed

    • Summary: getSupportForAttributes()
      Description: Indicates whether the underlying API supports retrieving attributes/columns for the target DB type.

    • File: src/Migration/Sources/Appwrite/Reader/API.php

    • Snippet location: getSupportForAttributes(): bool

  • src/Migration/Sources/Appwrite.php

    • Summary: getDatabase(string $databaseType, array $database), getEntity(string $databaseType, array $entity), getRecord(string $databaseType, array $record), getColumn(Table $table, mixed $column)
      Description: Factory/symmetry methods that map a given database type to a corresponding resource implementation:
      • For DocumentsDB (Resource::TYPE_DATABASE_DOCUMENTSDB) use DocumentsDB::fromArray(...)
      • For VectorDB (Resource::TYPE_DATABASE_VECTORDB) use VectorDB::fromArray(...)
      • Default falls back to Database::fromArray(...)
      • getEntity maps to Collection::fromArray(...) for DocumentsDB/VectorDB or Table::fromArray(...) otherwise
      • getRecord maps to Document::fromArray(...) for DocumentsDB/VectorDB or Row::fromArray(...) otherwise
      • getColumn builds a Column or Attribute instance depending on the column type
        Returns: respective Resource instances (Database/Collection/Table, Document/Row, etc.)
    • File: src/Migration/Sources/Appwrite.php
    • Snippet location: public static function getDatabase(string $databaseType, array $database): Resource
    • Snippet location: public static function getEntity(string $databaseType, array $entity): Resource
    • Snippet location: public static function getRecord(string $databaseType, array $record): Resource
    • Snippet location: public static function getColumn(Table $table, mixed $column): Column

Notes:

  • The Appwrite reader/API integration includes handling for DocumentsDB and VectorDB as database types. The user comment about “documentsdb will not be present here as there is no attribute support in documentsdb” is consistent with the architecture seen in the Appwrite-related code paths, where:
    • DocumentsDB is treated as a separate database type and typically has limited attribute/field support compared to legacy/VectorDB paths.
    • Attribute/Column handling for DocumentsDB is distinguished from other databases (as reflected by the getColumn/getAttribute dispatch logic).
  • If you need exact line references for each snippet above in your review, please specify and I can pin each to the closest line number in the provided repository context.

✏️ Learnings added
Learnt from: ArnabChatterjee20k
Repo: utopia-php/migration PR: 139
File: src/Migration/Sources/Appwrite.php:893-901
Timestamp: 2026-02-10T05:49:12.953Z
Learning: DocumentsDB (Resource::TYPE_DATABASE_DOCUMENTSDB) does not support attributes or columns. The DATABASE_TYPE_RESOURCE_MAP includes a 'field' => TYPE_ATTRIBUTE mapping for DocumentsDB only for structural consistency in the reader, but in practice DocumentsDB collections will not have attributes and the destination explicitly skips them with the message 'Columns not supported for DocumentsDB'.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: ItzNotABug
Repo: utopia-php/migration PR: 80
File: src/Migration/Sources/Appwrite.php:843-851
Timestamp: 2025-06-28T09:47:08.333Z
Learning: In the utopia-php/migration codebase, during the terminology swap from Collection/Attribute/Document to Table/Column/Row, the class constructors and method parameters use the new terminology (like "relatedTable"), but the underlying data structures and API responses still use the legacy keys (like "relatedCollection"). This is an intentional design pattern to allow gradual migration while maintaining compatibility with existing data sources.

Learnt from: ItzNotABug
Repo: utopia-php/migration PR: 80
File: src/Migration/Sources/Supabase.php:300-308
Timestamp: 2025-06-28T09:47:58.757Z
Learning: In the utopia-php/migration codebase, during the terminology swap from Collection/Attribute/Document to Table/Column/Row, the user ItzNotABug prefers to keep the existing query logic unchanged even if it becomes semantically incorrect with the new naming. The focus is purely on resource type renaming, not on fixing logical issues that become apparent after the terminology change.

Learnt from: ArnabChatterjee20k
Repo: utopia-php/migration PR: 114
File: src/Migration/Resources/Database/Attribute/Decimal.php:10-38
Timestamp: 2025-11-20T13:07:39.293Z
Learning: In the utopia-php/migration codebase, the user ArnabChatterjee20k maintains consistency between parallel implementations: Attribute classes (e.g., Attribute/Decimal.php) mirror their corresponding Column classes (e.g., Columns/Decimal.php) in constructor defaults and behavior, even when design decisions might be questioned.

Learnt from: ItzNotABug
Repo: utopia-php/migration PR: 80
File: src/Migration/Resources/Database/Columns/Relationship.php:86-89
Timestamp: 2025-06-28T09:45:57.650Z
Learning: In the utopia-php/migration codebase Relationship column class, the `getRelatedTable()` method intentionally returns `$this->options['relatedCollection']` (not `relatedTable`) because the underlying API still uses "collection" terminology, even though the internal codebase has been refactored to use "table" terminology.

Learnt from: ArnabChatterjee20k
Repo: utopia-php/migration PR: 114
File: src/Migration/Resources/Database/Collection.php:14-37
Timestamp: 2025-11-20T13:12:43.482Z
Learning: In the utopia-php/migration codebase, when Collection::fromArray is called, the input array is guaranteed to contain both the 'database' key and the nested 'type' key ($array['database']['type']), so defensive guards for these keys are not necessary.

Learnt from: ItzNotABug
Repo: utopia-php/migration PR: 80
File: src/Migration/Resources/Database/Row.php:60-60
Timestamp: 2025-06-28T09:45:36.026Z
Learning: In the utopia-php/migration codebase, the `fromArray` method is not used on Row objects, so mismatches between `jsonSerialize()` output keys and `fromArray()` input expectations for Row class are not problematic.

Learnt from: premtsd-code
Repo: utopia-php/migration PR: 142
File: src/Migration/Destinations/Appwrite.php:1542-1635
Timestamp: 2026-02-03T15:50:17.680Z
Learning: This repository targets Appwrite PHP SDK v19, and the migration code relies on enum classes Appwrite\Enums\Framework, Appwrite\Enums\BuildRuntime, and Appwrite\Enums\Adapter being present in vendor/appwrite/appwrite/src/Appwrite/Enums. The Sites mappings in src/Migration/Destinations/Appwrite.php intentionally follow the same enum pattern used for Functions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant